package fregat.base.net;


import java.io.*;
import java.net.Socket;
import java.util.*;

import fregat.base.util.ConfigurationBundle;
import fregat.base.util.Logger;

/**
 * Base class for applications that run as service and allow connection with
 * a remote or local console.
 *
 * @author Alexander Mann <br><b><i>ECO_L O G</i> &nbsp Logistiksysteme GmbH</b>
 * @version 1.0
 */
public abstract class ServiceLauncher {
  private static ResourceBundle resource = ResourceBundle.getBundle("at.ecolog.base.net.resource");
  private Hashtable shutdownRequested; // shutdown request for each console
  private Hashtable scope; // stores scope for each console
  private RemoteThread remoteThread; // thread that accepts remote command console
  private Hashtable history; // last entered commands for each console
  private static final int HISTORY_SIZE = 20; // amount of commands that are stored in history
  private static File lockFile; // lock file to avoid duplicate startup

  /**
   * Vector that stores available user commands
   */
  protected Vector commands = new Vector();

  /**
   * Logging category of service
   */
  public static String LOGGING_CATEGORY = Logger.DEFAULT_CATEGORY;

  /**
   * True, when shutdown was initiated
   */
  protected boolean shutdown;

  /**
   * Is called when a command was entered on console and is not one of the
   * default commands.
   *
   * @param input Input of console
   * @param out   Out stream where to write responses
   */
  protected abstract void parseInput(String input, PrintStream out);

  /**
   * Defines the commands for remote or local console
   */
  public ServiceLauncher() {
    defineCommands();
  }

  /**
   * @return Name and version of service
   */
  protected abstract String getName();

  /**
   * Reads the configuration and checks whether remote or local console are
   * enabled.<br><br>
   * &lt;configPath&gt;.console.remote = true<br> to enable remote console
   * &lt;configPath&gt;.console.remote.port = &lt;port&gt;<br> port of remote service
   * &lt;configPath&gt;.console.local = true<br> to enable local console
   *
   * @param configuration Configuration of service
   * @param configPath    Key prefix of console configuration
   * @param defaultPort   Default port of remote console
   */
  protected void start(ConfigurationBundle configuration, String configPath, int defaultPort) {

    // write lock file
    //if (configuration.getConfigurationBoolean("ServiceLauncher.NoLock")) {
      lockFile = null;
    //}
    writeLock();


    if (configuration.getConfigurationBoolean(configPath + ".console.remote")) {
      int port = configuration.getConfigurationInteger(configPath + ".console.remote.port");
      if (port <= 0) {
        port = defaultPort;
      }
      remoteThread = new RemoteThread(port);
      remoteThread.start();
      System.out.println(resource.getString("ServiceLauncher_start_1") + " " + port);
    }

    // add shutdown hook
    Runtime.getRuntime().addShutdownHook(new Thread() {
      public void run() {
        if (!shutdown) {
          shutdown();
        }
      }
    });

    if (configuration.getConfigurationBoolean(configPath + ".console.local")) {
      // start reading from input
      new Thread("ReadFromConsole") {
        public synchronized void run() {
          try {
            // sleep some time, as console is blocked
            wait(2000);
          } catch (InterruptedException ex) {
          }
          readFromConsole();
        }
      }.start();
    }
  }

  public static void setLockFile(String name) {
    lockFile = new File(System.getProperty("user.dir"), name + ".lock");
  }

  private static String getLockFile() {

    return lockFile.getName();
  }

  private static void writeLock() {
    try {
      lockFile.createNewFile();
    } catch (IOException ex) {
      Logger.getDefaultCategory().error("write," + getLockFile(), ex);
    } catch (NullPointerException ex) {
    }
  }

  public static boolean checkLock() {
    boolean exists = false;
    if (lockFile != null) {
      exists = lockFile.exists();
    }
    if (exists) {
      System.out.println(resource.getString("ServiceLauncher_checkLock_1").replaceFirst("%1", getLockFile()));
    }
    return exists;
  }

  private static void deleteLock() {
    if (lockFile != null) {
      lockFile.delete();
    }
  }

  /**
   * Tries to shutdown service by sending the 'shutdown' command to remote
   * console.
   *
   * @param configuration Configuration of service
   * @param configPath    Key prefix of console configuration
   * @param defaultPort   Default port of remote console
   */
  protected static void shutdown(ConfigurationBundle configuration, String configPath, int defaultPort) {
    try {
      int consolePort = 0;

      // get specified port from property file
      if (configuration != null) {
        consolePort = configuration.getConfigurationInteger(configPath + ".console.remote.port");
      }

      if (consolePort == 0) {
        consolePort = defaultPort;
        System.out.println(resource.getString("ServiceLauncher_shutdown_1")
          .replaceFirst("%1", Integer.toString(defaultPort)));
      }

      // use default port if no properties given
      Socket remoteSocket = new Socket("localhost", consolePort);
      remoteSocket.setTcpNoDelay(true);
      PrintStream out = new PrintStream(new DataOutputStream(remoteSocket.getOutputStream()));
      BufferedReader in = new BufferedReader(new InputStreamReader(remoteSocket.getInputStream()));

      // read welcome message and command list
      while (in.readLine().indexOf("?") < 0) ;

      // initiate immediate shutdown
      out.println("shutdown service");

      // wait some time to avoid socket exception on remote server
      try {
        Thread.sleep(1000);
      } catch (InterruptedException ex) {
      }

      remoteSocket.close();

      System.out.println(resource.getString("ServiceLauncher_shutdown_2"));

    } catch (IOException ex) {
      System.out.println(resource.getString("ServiceLauncher_shutdown_3"));
    }
  }

  /**
   * Defines available user commands by adding the definition strings to
   * {@link #commands}. The processing of the commands is implemented in
   * {@link #parseInput(java.lang.String,java.io.PrintStream)}.
   */
  protected void defineCommands() {
    commands.addElement(resource.getString("ServiceLauncher_defineCommands_1"));
    commands.addElement(resource.getString("ServiceLauncher_defineCommands_2"));
    commands.addElement(resource.getString("ServiceLauncher_defineCommands_3"));
    commands.addElement(resource.getString("ServiceLauncher_defineCommands_4"));
    commands.addElement(resource.getString("ServiceLauncher_defineCommands_5"));
    commands.addElement(resource.getString("ServiceLauncher_defineCommands_6"));
    commands.addElement(resource.getString("ServiceLauncher_defineCommands_7"));
  }

  /**
   * Enables command console on local input.<br><br>
   * <b>Warning:</b> If module server is running under Linux as a service, do
   * not enable the local command console as this causes high CPU cost without
   * any use!
   */
  public void readFromConsole() {
    BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));

    String input = "";
    try {
      do {
        input = stdIn.readLine();
        if (input != null) {
          handleInput(input, System.out);
        }
      } while (!shutdown);
      stdIn.close();
    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }

  /**
   * @param out Out stream of console
   * @return True, if shutdown was requested from console
   */
  private boolean shutdownRequested(PrintStream out) {
    if (shutdownRequested != null) {
      Boolean value = (Boolean) shutdownRequested.get(out);
      return value != null && value.booleanValue();
    }
    return false;
  }

  /**
   * This method handles user commands. The default implementation is able to
   * <li>Turn on/off console logging (LOG/NOLOG)</li>
   * <li>Filter logging (FILTER [&lt;String 1&gt; &lt;String 2&gt; ...])</li>
   * <li>Shutdown of service (SHUTDOWN)</li>
   * <li>Display list of commands (?)</li>
   *
   * @param input User input to parse
   * @param out   Stream where output is printed to
   */
  private void handleInput(String input, PrintStream out) {
    try {
      // reset shutdown request on any other command
      if (shutdownRequested(out) && !input.equalsIgnoreCase("SHUTDOWN")) {
        shutdownRequested.remove(out);
      }

      // process input
      if (input.equalsIgnoreCase("")) {
        // enter was pressed
        return;

      } else if (input.startsWith(".")) {
        // command history
        if (history != null && history.containsKey(out)) {
          HistoryItem item = (HistoryItem) history.get(out);
          Vector commands = item.commands;
          String parameter = input.substring(1).trim();

          if (parameter.length() > 0) {
            if (parameter.equals("?")) {
              // display command history
              for (int i = 0; i < commands.size(); i++) {
                println(i + ": " + commands.elementAt(i).toString(), out);
              }
            } else {
              // repeat command with index
              int i = Integer.parseInt(parameter);
              handleInput(commands.elementAt(i).toString(), out);
            }
          } else {
            // repeat last command
            handleInput(commands.elementAt(item.index).toString(), out);
          }
        }

      } else if (input.toUpperCase().startsWith("LOG")) {
        // enable logging to (remote) console
        StringTokenizer tokenizer = new StringTokenizer(input);
        tokenizer.nextToken();

        int intLevel = Logger.DEBUG; // default level

        if (tokenizer.hasMoreTokens()) {
          // level specified
          try {
            intLevel = Integer.parseInt(tokenizer.nextToken());
          } catch (NumberFormatException ex) {
          }
        }

        if (tokenizer.hasMoreTokens()) {
          // category specified
          String category = tokenizer.nextToken();
          if (Logger.exists(category)) {
            Logger.getCategory(category).addOut(out, intLevel);
          }
        } else {
          // change all categories
          Enumeration categories = Logger.getCategories();
          while (categories.hasMoreElements()) {
            ((Logger) categories.nextElement()).addOut(out, intLevel);
          }
        }

      } else if (input.toUpperCase().startsWith("NOLOG")) {
        // disable logging to (remote) console
        StringTokenizer tokenizer = new StringTokenizer(input);
        tokenizer.nextToken();

        if (tokenizer.hasMoreTokens()) {
          // category specified
          String category = tokenizer.nextToken();
          if (Logger.exists(category)) {
            Logger.getCategory(category).removeOut(out);
          }
        } else {
          // remove all categories
          Enumeration categories = Logger.getCategories();
          while (categories.hasMoreElements()) {
            ((Logger) categories.nextElement()).removeOut(out);
          }
        }

      } else if (input.toUpperCase().startsWith("FILTER")) {
        // add filter to logging
        StringTokenizer tokenizer = new StringTokenizer(input);
        tokenizer.nextToken();

        String[] strFilter = null;
        int i = 0;
        while (tokenizer.hasMoreTokens()) {
          if (strFilter == null) {
            strFilter = new String[tokenizer.countTokens()];
          }
          strFilter[i++] = tokenizer.nextToken();
        }
        Logger.getCategory(LOGGING_CATEGORY).setFilter(out, strFilter);

      } else if (input.toUpperCase().startsWith("SHUTDOWN")) {
        // shutdown initiated
        if (shutdownRequested(out) ||
          out == System.out ||
          input.equalsIgnoreCase("SHUTDOWN SERVICE")) {
          // stop the service
          // (if initiated on local console or by service shutdown immediate)
          debug(resource.getString("ServiceLauncher_handleInput_1"));
          shutdown();
        } else {
          // request shutdown (wait for repeated command)
          out.println(resource.getString("ServiceLauncher_handleInput_2") + "\r");
          if (shutdownRequested == null) {
            shutdownRequested = new Hashtable();
          }
          shutdownRequested.put(out, new Boolean(true));
        }
      } else if (input.equalsIgnoreCase("VERSION")) {
        // display version of service
        println(getName(), out);

      } else {
        // command scope -->
        StringTokenizer tokenizer = new StringTokenizer(input, " ");
        String command = tokenizer.nextToken(); // command
        String target = null;
        if (tokenizer.hasMoreTokens()) {
          target = tokenizer.nextToken(); // target of command
        }

        if (scope == null) {
          scope = new Hashtable();
        }

        if (command.equalsIgnoreCase("CMD")) {
          if (!tokenizer.hasMoreTokens() && target != null) {
            // CMD <target> sets scope
            scope.put(out, input);
            println(resource.getString("ServiceLauncher_handleInput_3").replaceFirst("%1", target), out);
          } else if (scope.containsKey(out)) {
            // any other command with CMD removes the scope
            scope.remove(out);
            println(resource.getString("ServiceLauncher_handleInput_4"), out);
          }
          if (!tokenizer.hasMoreTokens()) {
            return;
          }
        }

        if (scope.containsKey(out)) {
          input = scope.get(out).toString() + " " + input;
        }
        // <--

        if (input.equalsIgnoreCase("?")) {
          // show list of commands
          for (int i = 0; i < commands.size(); i++) {
            println((String) commands.elementAt(i), out);
          }
          return;
        }

        parseInput(input, out);

        if (history == null) {
          history = new Hashtable();
        }

        HistoryItem item;
        if (history.containsKey(out)) {
          item = (HistoryItem) history.get(out);
        } else {
          item = new HistoryItem();
          history.put(out, item);
        }
        if (!item.commands.contains(input)) {
          item.commands.addElement(input);
          item.index = item.commands.size() - 1;
        } else {
          item.index = item.commands.indexOf(input);
        }
        if (item.commands.size() > HISTORY_SIZE) {
          item.commands.removeElementAt(0);
        }
      }
    } catch (Exception ex) {
      // log exception
      error("ServiceLauncher.handleInput", ex);
    }
  }

  public void shutdown() {
    shutdown = true;

    // delete lock file
    deleteLock();

    // stop remote thread if enabled
    if (remoteThread != null) {
      remoteThread.shutdown();
    }

    // stop all loggers
    Logger.shutdownAll();
  }


  /**
   * Central method for debug output.
   *
   * @param str String to log
   */
  public static void debug(String str) {
    Logger.getCategory(LOGGING_CATEGORY).debug(str);
  }

  /**
   * Central method for error output
   *
   * @param str String to log
   */
  public static void error(String str) {
    Logger.getCategory(LOGGING_CATEGORY).error(str);
  }

  /**
   * Central method for logging exception
   *
   * @param str Praefix
   * @param ex  Exception
   */
  public static void error(String str, Exception ex) {
    Logger.getCategory(LOGGING_CATEGORY).error(str, ex);
  }

  /**
   * Central method for warning output
   *
   * @param str String to log
   */
  public static void warn(String str) {
    Logger.getCategory(LOGGING_CATEGORY).warn(str);
  }

  /**
   * Prints the given String to output. If output is other than
   * lcoal console '\r' is added.
   *
   * @param str String to print
   * @param out Output stream
   */
  public static void println(String str, PrintStream out) {
    if (out != System.out) {
      str += "\r";
    }
    out.println(str);
  }

  /**
   * Central method for note output
   *
   * @param str String to log
   */
  public static void note(String str) {
    Logger.getCategory(LOGGING_CATEGORY).note(str);
  }

  /**
   * Central method for info output
   *
   * @param str String to log
   */
  public static void info(String str) {
    Logger.getCategory(LOGGING_CATEGORY).info(str);
  }

  /**
   * Accepts remote command console connections and forward input to processing
   * method.
   */
  private class RemoteThread extends RemoteServer {
    public RemoteThread(int intPort) {
      super(intPort, ServiceLauncher.this.getName());
    }

    protected void parseInput(String input, PrintStream out) {
      ServiceLauncher.this.handleInput(input, out);
    }

    protected void clientShutdown(PrintStream out) {
      Logger.getCategory(LOGGING_CATEGORY).removeOut(out);
    }

    protected void clientConnected(PrintStream out) {
      //this.parseInput("LOG",out);
      out.println("\r");
      this.parseInput("?", out);
    }

    public void shutdown() {
      super.shutdown();
    }
  }

  private class HistoryItem {
    Vector commands;
    int index;

    public HistoryItem() {
      commands = new Vector(HISTORY_SIZE);
    }
  }
}